/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.plugin.maven.server.rmi;

import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.plugin.maven.server.execution.ExecutionException;
import org.eclipse.che.plugin.maven.server.execution.ProcessEvent;
import org.eclipse.che.plugin.maven.server.execution.ProcessExecutor;
import org.eclipse.che.plugin.maven.server.execution.ProcessHandler;
import org.eclipse.che.plugin.maven.server.execution.ProcessListener;
import org.eclipse.che.plugin.maven.server.execution.ProcessOutputType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.rmi.PortableRemoteObject;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

/**
 * @author Evgen Vidolob
 */
public abstract class RmiClient<Remote> {
    private static final Logger LOG = LoggerFactory.getLogger(RmiClient.class);

    private final Class<Remote> remoteClass;
    private final Map<Pair<Object, Object>, ProcessInfo> infoMap = new HashMap<>();

    public RmiClient(Class<Remote> remoteClass) {
        this.remoteClass = remoteClass;
    }

    public Remote acquire(Object target, Object param) throws Exception {
        Ref<RunningInfo> ref = Ref.ofNull();
        Pair<Object, Object> key = Pair.of(target, param);
        if (!getInfo(ref, key)) {
            startProcess(key);
            if (ref.isNull()) {
                try {
                    synchronized (ref) {
                        while (ref.isNull()) {
                            ref.wait(1000);
                        }
                    }
                } catch (InterruptedException ignored) {
                }
            }
        }

        if (ref.isNull()) {
            throw new RuntimeException("Can't get remote proxy for: " + target.toString());
        }
        RunningInfo info = ref.getValue();
        if (info.processHandler == null) {
            throw new Exception(info.name);
        }

        return acquire(info);
    }

    private Remote acquire(RunningInfo info) throws Exception {
        Remote result;
        Registry registry = LocateRegistry.getRegistry("localhost", info.port);
        java.rmi.Remote lookup = registry.lookup(info.name);
        if (java.rmi.Remote.class.isAssignableFrom(remoteClass)) {
            Remote entry = remoteClass.isInstance(lookup) ? (Remote)lookup : (Remote)PortableRemoteObject.narrow(lookup, remoteClass);
            if (entry == null) {
                result = null;
            } else {
                //TODO add proxy for remote object
                result = (Remote)lookup;
            }
        } else {
            result = (Remote)lookup;
        }

        info.remoteRef = result;
        return result;
    }

    private void startProcess(Pair<Object, Object> key) {
        ProcessHandler handler;
        try {
            ProcessExecutor executor = getExecutor();
            handler = executor.execute();
        } catch (ExecutionException e) {
            removeProcessInfo(key, null, e.getMessage());
            return;
        }
        handler.addProcessListener(getProcessListener(key));
        handler.startNotify();
    }

    private ProcessListener getProcessListener(Pair<Object, Object> key) {
        return new ProcessListener() {
            @Override
            public void onStart(ProcessEvent event) {
                ProcessHandler processHandler = event.getProcessHandler();
                synchronized (infoMap) {
                    ProcessInfo info = infoMap.get(key);
                    if (info instanceof PendingInfo) {
                        infoMap.put(key, new PendingInfo(processHandler, ((PendingInfo)info).ref));
                    }
                }
            }

            @Override
            public void onText(ProcessEvent event, ProcessOutputType outputType) {
                String text = event.getText();
                if (text == null) {
                    text = "";
                }
                if (outputType == ProcessOutputType.STDERR) {
                    LOG.error(text);
                } else {
                    LOG.info(text);
                }

                RunningInfo runningInfo = null;
                PendingInfo pendingInfo = null;
                synchronized (infoMap) {
                    ProcessInfo info = infoMap.get(key);
                    if (info instanceof PendingInfo) {
                        pendingInfo = (PendingInfo)info;
                        if (outputType == ProcessOutputType.STDOUT) {
                            String prefix = "Port/Name:";
                            if (text.startsWith(prefix)) {
                                String portName = text.substring(prefix.length()).trim();
                                int i = portName.indexOf('/');
                                runningInfo = new RunningInfo(pendingInfo.processHandler, Integer.parseInt(portName.substring(0, i)),
                                                              portName.substring(i + 1));
                                infoMap.put(key, runningInfo);
                                infoMap.notifyAll();
                            }
                        } else if (outputType == ProcessOutputType.STDERR) {
                            pendingInfo.stderr.append(text);
                        }
                    }
                }

                if (runningInfo != null) {
                    synchronized (pendingInfo.ref) {
                        pendingInfo.ref.setValue(runningInfo);
                        pendingInfo.ref.notifyAll();
                    }
                    // todo add ping there
                }


            }

            @Override
            public void onProcessTerminated(ProcessEvent event) {
                removeProcessInfo(key, event.getProcessHandler(), null);
            }

            @Override
            public void onProcessWillTerminate(ProcessEvent event) {
                removeProcessInfo(key, event.getProcessHandler(), null);
            }
        };
    }

    private boolean removeProcessInfo(Pair<Object, Object> key, ProcessHandler handler, String message) {
        ProcessInfo info;
        synchronized (infoMap) {
            info = infoMap.get(key);
            if (info != null && (handler == null || info.processHandler == handler)) {
                infoMap.remove(key);
                infoMap.notifyAll();
            } else {
                info = null;
            }
        }

        if (info instanceof PendingInfo) {
            PendingInfo pendingInfo = (PendingInfo)info;
            if (pendingInfo.stderr.length() > 0 || pendingInfo.ref.isNull()) {
                if (message != null) {
                    pendingInfo.stderr.append(message);
                }
                pendingInfo.ref.setValue(new RunningInfo(null, -1, pendingInfo.stderr.toString()));
            }

            synchronized (pendingInfo.ref) {
                pendingInfo.ref.notifyAll();
            }
        }

        return info != null;
    }

    protected abstract ProcessExecutor getExecutor();

    private boolean getInfo(Ref<RunningInfo> ref, Pair<Object, Object> key) {
        ProcessInfo info;
        synchronized (infoMap) {
            info = infoMap.get(key);
            try {
                while (info != null && (!(info instanceof RunningInfo) ||
                                        info.processHandler.isProcessTerminating() ||
                                        info.processHandler.isProcessTerminated())) {
                    infoMap.wait(1000);
                    info = infoMap.get(key);
                }
            } catch (InterruptedException ignore) {
            }

            if (info == null) {
                infoMap.put(key, new PendingInfo(null, ref));
            }
        }

        if (info instanceof RunningInfo) {
            synchronized (ref) {
                ref.setValue((RunningInfo)info);
                ref.notifyAll();
            }
        }
        return info != null;
    }

    public void stopAll(boolean wait) {
        List<ProcessInfo> processList;
        synchronized (infoMap) {
            processList = (infoMap.values().stream().filter(info -> info.processHandler != null).collect(Collectors.toList()));
        }

        if (processList.isEmpty()) {
            return;
        }

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<?> future = executorService.submit(() -> {
            for (ProcessInfo processInfo : processList) {
                processInfo.processHandler.destroyProcess();
            }

            if (wait) {
                for (ProcessInfo processInfo : processList) {
                    processInfo.processHandler.waitFor();
                }
            }
        });

        if (wait) {
            try {
                future.get();
            } catch (InterruptedException ignore) {
            } catch (java.util.concurrent.ExecutionException e) {
                LOG.error(e.getMessage(), e);
            }
        }
        executorService.shutdown();
    }

    private static class ProcessInfo {
        final ProcessHandler processHandler;

        public ProcessInfo(ProcessHandler processHandler) {
            this.processHandler = processHandler;
        }
    }

    private static class RunningInfo extends ProcessInfo {
        final int    port;
        final String name;
        Object remoteRef;

        public RunningInfo(ProcessHandler processHandler, int port, String name) {
            super(processHandler);
            this.port = port;
            this.name = name;
        }

        @Override
        public String toString() {
            return "RunnigInfo{" +
                   "port=" + port +
                   ", name='" + name + '\'' +
                   '}';
        }
    }

    private static class PendingInfo extends ProcessInfo {
        final Ref<RunningInfo> ref;
        final StringBuilder stderr = new StringBuilder();

        public PendingInfo(ProcessHandler processHandler,
                           Ref<RunningInfo> ref) {
            super(processHandler);
            this.ref = ref;
        }

        @Override
        public String toString() {
            return "PendingInfo{" +
                   "ref=" + ref +
                   '}';
        }
    }

    static {
        //set up RMI
        System.setProperty("java.rmi.server.hostname", "localhost");
        System.setProperty("java.rmi.server.disableHttp", "true");
    }
}
